﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading;

namespace Fadd.Components.Remoting
{
	/// <summary>
	/// Transports method invocations to a remote party.
	/// </summary>
	/// <remarks>
	/// <para>
	/// A RemotingChannel will transport method invocation requests from a client to server,
	/// where they are executed (and result is sent back).
	/// </para>
	/// <para>
	/// The channel is not fully built yet. Currently it only support methods without <c>ref</c> and <c>out</c> keywords.
	/// It supports return values as long as they can be serialized.
	/// </para>
	/// </remarks>
	/// <example>
	/// <code>
	/// <![CDATA[
	/// private void Remoting()
	/// {
	/// 	// This is typically done in your server.
	/// 	ComponentManager server = SetupServerRemoting();
	///
	/// 	// Typically done in your client applications.
	/// 	ComponentManager client = SetupClientRemoting();
	///
	/// 	// Invoke a method in your client to get it executed in your server.
	/// 	string myMessages = client.Get<IMessageManager>().GetMessages();
	/// }
	///
	/// private ComponentManager SetupClientRemoting()
	/// {
	/// 	// We'll create a new component manager for this example only.
	/// 	// Normally you have already created a component manager in your system,
	/// 	// which also is used for the remoting.
	/// 	ComponentManager clientManager = new ComponentManager {Location = "Client"};
	///
	/// 	// Find all components in our current assembly. 
	/// 	// ComponentManager will only add components with the correct RunAt property.
	/// 	ComponentFinder finder = new ComponentFinder();
	/// 	finder.Find(new List<Assembly>() { GetType().Assembly });
	/// 	clientManager.Add(finder.Components);
	///
	/// 	// Define where we should connect
	/// 	RemotingChannel client = new RemotingChannel(clientManager, false);
	/// 	client.Start(new IPEndPoint(IPAddress.Loopback, 8334));
	///
	/// 	return clientManager;
	/// }
	///
	/// private ComponentManager SetupServerRemoting()
	/// {
	/// 	// We'll create a new component manager for this example only.
	/// 	// Normally you have already created a component manager in your system,
	/// 	// which also is used for the remoting.
	/// 	ComponentManager manager = new ComponentManager { Location = "Server" };
	///
	/// 	// Find all components in our current assembly. 
	/// 	// ComponentManager will only add components with the correct RunAt property.
	/// 	ComponentFinder finder = new ComponentFinder();
	/// 	finder.Find(new List<Assembly>() { GetType().Assembly });
	/// 	manager.Add(finder.Components);
	///
	/// 	// Setup remoting, we should accept connections on port 8834.
	/// 	RemotingChannel server = new RemotingChannel(_manager, true);
	/// 	server.Start(new IPEndPoint(IPAddress.Loopback, 8334));
	///
	/// 	return manager;
	/// }
	/// ]]>
	/// </code>
	/// </example>
	public class RemotingChannel
	{
		private readonly byte[] _buffer = new byte[8192];
		private readonly ComponentManager _components;
		private readonly BinaryFormatter _formatter = new BinaryFormatter();
		private readonly MemoryStream _inputStream = new MemoryStream();
		private readonly Dictionary<int, Request> _requests = new Dictionary<int, Request>();
		private int _bytesLeft;
	    private int _bytesToProcess;
		private IPEndPoint _endPoint;
		private int _functionId;
		private bool _shuttingDown;
		private Socket _socket;
		private Timer _timer;
		private int _transactionId;
		private int _readStep;
		private bool _isRunningInServer;

		/// <summary>
		/// Initializes a new instance of the <see cref="RemotingChannel"/> class.
		/// </summary>
		/// <param name="manager">Manager used to invoke commands (if server side).</param>
		public RemotingChannel(ComponentManager manager)
		{
			_components = manager;
			manager.AddInstance<RemotingChannel>(this);
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="RemotingChannel"/> class.
		/// </summary>
		/// <param name="manager">Manager used to invoke commands (if server side).</param>
		/// <param name="socket">Socket connected to client.</param>
		internal RemotingChannel(ComponentManager manager, Socket socket)
		{
			_components = manager;
			_isRunningInServer = true;
			_socket = socket;
			BeginRead();
		}

		/// <summary>
		/// Start channel
		/// </summary>
		/// <param name="value">End point to connect to or listen on.</param>
		public void Start(IPEndPoint value)
		{
			_endPoint = value;
			ConnectClient(value);
		}

		private void ConnectClient(EndPoint value)
		{
			_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
			_socket.Connect(value);
			BeginRead();
		}
        
		private void BeginRead()
		{
			_socket.BeginReceive(_buffer, 0, _buffer.Length, SocketFlags.None, OnReceive, null);
		}

		private void OnReceive(IAsyncResult ar)
		{
			if (_socket == null)
				return;

			try
			{
				int bytesRead = _socket.EndReceive(ar);
				if (bytesRead == 0)
				{
					HandleDisconnect();
					return;
				}

				HandleReceive(bytesRead);
				Receive(_bytesToProcess);
			}
			catch(Exception)
			{
				HandleDisconnect();
			}
        }

		/// <summary>
		/// Handle received bytes.
		/// </summary>
		/// <param name="bytesRead">Number of bytes received</param>
		protected virtual void HandleReceive(int bytesRead)
		{
		    _bytesToProcess += bytesRead;
		    Debug("Bytes read: " + _bytesToProcess);
			int offset = 0;
            while (offset < _bytesToProcess)
			{
				// need function id
				if (_readStep == 0)
				{
                    if (_bytesToProcess < 8)
						return;

					_functionId = BitConverter.ToInt32(_buffer, 0);
					_bytesLeft = BitConverter.ToInt32(_buffer, 4);
					++_readStep;
					offset += 8;
				}
				else
				{
					// have more than enough bytes.
                    if (_bytesLeft <= _bytesToProcess - offset)
					{
						_inputStream.Write(_buffer, offset, _bytesLeft);
						_readStep = 0;
						offset += _bytesLeft;
						ProcessPacket();
					}
					else
					{
                        int bytesToRead = _bytesToProcess - offset;
						_inputStream.Write(_buffer, offset, bytesToRead);
						_bytesLeft -= bytesToRead;
						offset += bytesToRead;
					}
				}
			}

            if (offset < _bytesToProcess)
            {
                Buffer.BlockCopy(_buffer, offset, _buffer, 0, _bytesToProcess - offset);
                _bytesToProcess -= offset;
            }
            else
                _bytesToProcess = 0;
		}

	    protected virtual void Debug(string log)
	    {
#if TEST
            Console.WriteLine(_components.Location + ": " + log);
            System.Diagnostics.Debug.WriteLine(_components.Location + ": " + log);
#endif    
	    }

	    private void Receive(int offset)
		{
			_socket.BeginReceive(_buffer, offset, _buffer.Length - offset, SocketFlags.None, OnReceive, null);			
		}

		private void HandleDisconnect()
		{
			if (_shuttingDown)
				return;

			Disconnected(this, EventArgs.Empty);
			_readStep = 0;
			_bytesLeft = 0;
			_functionId = 0;
		    _bytesToProcess = 0;
			_socket.Close();
			_socket = null;

			if (!_isRunningInServer)
				_timer = new Timer(OnTryConnect, null, 15000, 15000);
		}

		/// <summary>
		/// Stop the channel.
		/// </summary>
		public void Stop()
		{
			_shuttingDown = true;
			if (_socket == null) return;
			_socket.Shutdown(SocketShutdown.Both);
			_socket.Disconnect(true);
			_socket = null;
		}

		private void OnTryConnect(object state)
		{
			if (_shuttingDown)
			{
				_timer.Dispose();
				_timer = null;
				return;
			}

			try
			{
				ConnectClient(_endPoint);
				_timer.Dispose();
				_timer = null;
			}
			catch (SocketException)
			{
			}
		}

		protected void ProcessPacket()
		{
			_inputStream.Flush();
			_inputStream.Seek(0, SeekOrigin.Begin);
		    Debug("Processing type: " + _functionId);
			switch (_functionId)
			{
				case 1:
					var call = (MethodCall) _formatter.Deserialize(_inputStream);
					ThreadPool.QueueUserWorkItem(InvokeMethod, call);
					break;
				case 2:
					Response response = (Response) _formatter.Deserialize(_inputStream);
					HandleMethodResponse(response);
					break;
			}
			_inputStream.SetLength(0);
		}

#if TEST
        protected virtual void AddToStream(byte[] buffer, int offset, int count)
        {
            _inputStream.Write(buffer, offset, count);
        }
#endif
		private void HandleMethodResponse(Response reply)
		{
			Request request;
			lock (_requests)
			{
				if (!_requests.TryGetValue(reply.RequestId, out request))
					return;
				_requests.Remove(reply.RequestId);
			}

			request.Response = reply;
			request.Event.Set();
		}

		/// <summary>
		/// Invoke a method at the remote end.
		/// </summary>
		/// <param name="state"></param>
		private void InvokeMethod(object state)
		{
			var call = (MethodCall) state;
			object instance = _components.Get(call.InterfaceType);

			Response reply;
			if (instance == null)
				reply = new MethodException(call.RequestId, new InvalidOperationException("Failed to find component on remote end."));
			else
			{
				try
				{
					MethodInfo method = instance.GetType().GetMethod(call.MethodName);
					if (method.IsGenericMethod)
						method = method.MakeGenericMethod(call.GenericArgumentTypes);
					object result = method.Invoke(instance, call.Arguments);
					reply = new MethodCallReply(call.RequestId) { Result = result};
				}
				catch (Exception err)
				{
					reply = new MethodException(call.RequestId, new TargetInvocationException("Failed to execute method.", err));
				}
			}


			byte[] buffer = Serialize(reply);
			Send(2, buffer);
		}

		/// <summary>
		/// Send buffer to remote end.
		/// </summary>
		/// <param name="functionId">Function to invoke</param>
		/// <param name="buffer">Buffer to send.</param>
		protected virtual void Send(Int32 functionId, byte[] buffer)
		{
            Debug("Sending function " + functionId + " of length " + buffer.Length);
			Int32 length = buffer.Length;
			_socket.Send(BitConverter.GetBytes(functionId));
			_socket.Send(BitConverter.GetBytes(length));
			_socket.Send(buffer, 0, buffer.Length, SocketFlags.None);
		}

		/// <summary>
		/// Invoke a method at the other end.
		/// </summary>
		/// <param name="interfaceType">Component that the method exist in</param>
		/// <param name="methodName">Method to invoke</param>
		/// <param name="parameters">Method parameters</param>
		/// <exception cref="InvalidOperationException">Failed to get answer from the server.</exception>
		/// <remarks>
		/// <para>
		/// Use other Invoke method if the method is generic.
		/// </para>
		/// </remarks>
		public object Invoke(Type interfaceType, string methodName, params object[] parameters)
		{
            if (_socket == null)
                throw new InvalidOperationException("Channel have not been started.");
			if (!_socket.Connected)
				throw new InvalidOperationException("Not connected to remote end.");

		    MethodInfo method = _components.Get(interfaceType).GetType().GetMethod(methodName);
            if (method != null && method.IsGenericMethod)
                throw new InvalidOperationException("Method is generic, use InvokeGeneric instead.");

			var call = new MethodCall
			           	{
			           		InterfaceType = interfaceType,
			           		MethodName = methodName,
			           		Arguments = parameters,
			           		RequestId = Interlocked.Increment(ref _transactionId)
			           	};

			var request = new Request {Event = new ManualResetEvent(false), Id = call.RequestId};
			_requests.Add(call.RequestId, request);

			Send(1, Serialize(call));

			if (!request.Event.WaitOne(5000))
				throw new InvalidOperationException("Failed to get answer from the server.");


			if (request.Response is MethodException)
				throw ((MethodException)request.Response).Error;

			return ((MethodCallReply) request.Response).Result;
		}


        /// <summary>
        /// Invoke a generic method at the other end.
        /// </summary>
        /// <param name="interfaceType">Component that the method exist in</param>
        /// <param name="methodName">Method to invoke</param>
        /// <param name="parameterTypes">All argument types, needed to find the </param>
        /// <param name="parameters">Method parameters</param>
        /// <exception cref="InvalidOperationException">Failed to get answer from the server.</exception>
        public object InvokeGeneric(Type interfaceType, string methodName, Type[] parameterTypes, object[] parameters)
        {
            if (_socket == null)
                throw new InvalidOperationException("Channel have not been started.");
            if (!_socket.Connected)
                throw new InvalidOperationException("Not connected to remote end.");

            var call = new MethodCall
            {
                InterfaceType = interfaceType,
                MethodName = methodName,
                GenericArgumentTypes = parameterTypes,
                Arguments = parameters,
                RequestId = Interlocked.Increment(ref _transactionId)
            };

            var request = new Request { Event = new ManualResetEvent(false), Id = call.RequestId };
            _requests.Add(call.RequestId, request);

            Send(1, Serialize(call));

            if (!request.Event.WaitOne(5000))
                throw new InvalidOperationException("Failed to get answer from the server.");

            return request.Response;
        }

		/// <summary>
		/// Binary serialize an object and return a byte array
		/// </summary>
		/// <param name="message">Object to serialize</param>
		/// <returns>byte array with the serialized object</returns>
		protected byte[] Serialize(object message)
		{
			var stream = new MemoryStream();
			_formatter.Serialize(stream, message);
			stream.Seek(0, SeekOrigin.Begin);
			stream.Flush();

			var buffer = new byte[stream.Length];
			stream.Read(buffer, 0, (int) stream.Length);
			stream.Dispose();
			return buffer;
		}

		/// <summary>
		/// Client got disconnected.
		/// </summary>
		public event EventHandler Disconnected = delegate { };
	}
}